import sys
import os
import random
import time

import pyglet
import cocos
from cocos import tiles
from cocos.director import director
from cocos.actions import *
from cocos.menu import *
from cocos.tiles import RectCell, Resource

from gamelib import npc, ui, party, los, success, music, dialog
from gamelib import popup_menu, inventory, menustyle, pathfinding

class CharacterLayer(tiles.ScrollableLayer):
    '''This is the layer that contains all the characters - player or otherwise.

    It manages the sprite for each character.
    '''
    def __init__(self):
        super(CharacterLayer, self).__init__()
        self.character_sprites = {}
        self.npc_sprites = {}

    def generate_sprite(self, character):
        sprite_name = character.sprite_name
        if character.is_dead():
            sprite_name = sprite_name.replace('.png', '-dead.png')
        image = pyglet.resource.image(sprite_name)
        image.anchor_x = image.width//2
        image.anchor_y = image.height//2

        map = self.parent.parent.ground
        s = cocos.sprite.Sprite(image, map.get_cell(*character.position).center)
        self.add(s, z=1)

        # add equipment in hands
        for item, offset in [(character.body.right_hand, 16),
                (character.body.left_hand, -16)]:
            if not item: continue
            i = pyglet.resource.image(item.sprite_name)
            i.anchor_x = i.width//2
            i.anchor_y = i.height//2
            item = cocos.sprite.Sprite(i, (offset, -8))
            s.add(item, z=1)

        return s

    def add_character(self, character):
        '''Add a player character to the character display layer.
        '''
        s = self.generate_sprite(character)
        self.character_sprites[character.name] = s

        # have the character let us know when things about it change
        character.push_handlers(self)

    def add_npc(self, npc):
        '''Add a non-player character to the character display layer.
        '''
        s = self.generate_sprite(npc)
        self.npc_sprites[npc.name] = s

        # have the NPC let us know when things about it change
        npc.push_handlers(self)

    def get_at_pixel(self, x, y, only_alive=True):
        '''Determine which character is at the given pixel. Must be a direct hit
        on the actual sprite of the character, not the whole cell the character is
        in.
        '''
        level = self.parent.parent
        for list, mapping in [(level.npcs, self.npc_sprites),
                (level.party.characters, self.character_sprites)]:
            for thing in list:
                if not mapping[thing.name].contains(x, y): continue
                if only_alive and thing.is_dead(): continue
                return thing
        return None

    def get_at_cell(self, i, j, only_alive=True):
        '''Sometimes we need to ignore some characters.
        '''
        for list in (self.parent.parent.npcs, self.parent.parent.party.characters):
            for thing in list:
                if thing.position != (i, j): continue
                if only_alive and thing.is_dead(): continue
                return thing
        return None

    def on_exit(self):
        # clean up when this layer is no longer displayed
        super(CharacterLayer, self).on_exit()
        self.character_sprites = {}
        self.npc_sprites = {}

    def get_character_sprite(self, character):
        '''From a Character instance, find the PC's Sprite.
        '''
        return self.character_sprites[character.name]
    def get_npc_sprite(self, character):
        '''From a Character instance, find the NPC's Sprite.
        '''
        return self.npc_sprites[character.name]
    def get_sprite(self, character):
        '''From a Character instance, find the PC or NPC Sprite.
        '''
        if isinstance(character, npc.NPC):
            return self.npc_sprites[character.name]
        else:
            return self.character_sprites[character.name]

    def on_character_move(self, character, old_position, position):
        '''A character (NPC or PC) has moved and is letting us know.
        '''
        # callback while we're not still attached to a Scene :(
        if not self.parent:
            return
        level = self.parent.parent
        cell = level.get_cell(*position)

        self.get_sprite(character).do(MoveTo(cell.center, .1))

        level.check_npc_visibility()

        if character is level.active_character:
            level.scroller.set_focus(*cell.center)
            level.update_visible_npcs()

    def on_character_damage(self, character, old_health, damage):
        '''A character (NPC or PC) has taken damage.
        '''
        sprite = self.get_sprite(character)

        sprite.do(Blink(5, .5))

        # show how much
        layer = tiles.ScrollableLayer()
        images = []
        for c in str(abs(damage)):
            images.append(pyglet.resource.image('number-%s.png'%c))
        # sprite position is in the center
        x, y = sprite.position
        # all number images are the same height
        y -= images[0].height//2
        x -= sum(image.width for image in images) // 2
        level = self.parent.parent
        for image in images:
            sprite = cocos.sprite.Sprite(image, (x, y))
            sprite.image_anchor = (0,0)
            layer.add(sprite)
            x += image.width
            sprite.do(MoveBy((0, 64), 1) | FadeOut(1))
        level.scroller.add(layer, z=10)
        level.do(Delay(1) +
            CallFunc(lambda l=level,l2=layer:l.scroller.remove(l2)))

    def on_character_attack_miss(self, character, attack):
        '''The attack just missed on this character.
        '''
        # create a new layer with the miss icon in it
        layer = tiles.ScrollableLayer()
        image = pyglet.resource.image('icon-miss.png')

        # position the miss icon over the character
        sprite = cocos.sprite.Sprite(image, self.get_sprite(character).position)
        layer.add(sprite)
        level = self.parent.parent
        level.scroller.add(layer, z=10)
        level.do(
            (MoveBy((0, 64), 1) | FadeOut(1))
            +
            CallFunc(lambda l=level,l2=layer:l.scroller.remove(l2)),
            sprite
        )

    def on_character_death(self, character):
        '''A character (NPC or PC) has taken damage.
        '''
        # use the same sprite name but with -dead appended before the suffix
        sprite_name = character.sprite_name.replace('.png', '-dead.png')
        image = pyglet.resource.image(sprite_name)
        image.anchor_x = image.width//2
        image.anchor_y = image.height//2

        s = self.get_sprite(character)
        s.image = image

        # need the sprite to drop to the back, and this is the only way
        self.remove(s)
        self.add(s, z=0)

        # check success
        level = self.parent.parent
        if not [n for n in level.npcs if not n.is_dead()]:
            director.replace(success.Success(level))
            return

class GroundInfoLayer(tiles.ScrollableLayer):
    shown = None
    action_sprite = None
    action_cell = None
    action_icon = None
    active_character = None
    character_highlight = None

    def clear(self):
        # remove old
        if self.shown:
            for pos in dict(self.shown):
                self.remove(self.shown[pos])
                del self.shown[pos]
        if self.character_highlight:
            self.remove(self.character_highlight)
            self.character_highlight = None
        if self.action_sprite:
            self.remove(self.action_sprite)
            self.action_sprite = None

        self.action_icon = None
        self.action_cell = None
        self.active_character = None

    def update_for_character(self, character, walk_cells, action_cell, action):
        if self.shown is None:
            self.shown = {}

        if self.active_character and character is not self.active_character:
            self.clear()

        if self.action_cell != action_cell and self.action_sprite:
            self.clear()

        # show the action icon if there is one
        if action:
            if action.icon != self.action_icon:
                self.action_icon = action.icon
                image = pyglet.resource.image(action.icon)
                image.anchor_x = image.width//2
                image.anchor_y = image.height//2
                self.action_sprite = cocos.sprite.Sprite(image, action_cell.center)
                self.action_cell = action_cell
                self.add(self.action_sprite)
        elif self.action_sprite:
            self.remove(self.action_sprite)
            self.action_sprite = None
            self.action_cell = None
            self.action_icon = None

        # update walk icons
        image = pyglet.resource.image('icon-walk.png')
        image.anchor_x = image.width//2
        image.anchor_y = image.height//2

        # remove unnecessary old ones
        sp = set(walk_cells)
        for pos in dict(self.shown):
            if pos not in sp:
                self.remove(self.shown[pos])
                del self.shown[pos]

        # add new
        m = self.parent.parent.ground
        for pos in walk_cells:
            if pos in self.shown:
                continue
            cell = m.get_cell(*pos)
            s = cocos.sprite.Sprite(image, cell.center)
            self.add(s)
            self.shown[pos] = s

        # add character highlight
        if (self.character_highlight is None or
                self.character_highlight.cell != character.position):
            if self.character_highlight:
                self.remove(self.character_highlight)
            image = pyglet.resource.image('icon-green-box.png')
            image.anchor_x = image.width//2
            image.anchor_y = image.height//2
            cell = m.get_cell(*character.position)
            s = cocos.sprite.Sprite(image, cell.center)
            self.add(s)
            s.cell = character.position
            self.character_highlight = s


    def on_exit(self):
        super(GroundInfoLayer, self).on_exit()
        self.shown = None
        self.character_highlight = None


class LightSource(object):
    '''Represent a single light source that lights the given level.
    '''
    def __init__(self, position, strength, level):
        self.position = position
        self.strength = strength
        self.level = level

        self.light()

    def light(self):
        '''Fill out from the given cell (starting at the light source's
        position) and light the light map.

        Breadth-first to retain sanity.
        Also, looping is way faster than recursion.
        '''
        cells = [self.position]
        strength = self.strength
        self.light_map = {}
        visited = {}

        while cells:
            new = []
            for cell in cells:
                # grab the actual map cell for this coordinate
                cell_obj = self.level.ground.get_cell(*cell)

                # no point lighting if there's no map cell
                if cell_obj is None: continue

                # check the cell can see the light
                quality, path = los.line_of_sight(self.level, self.position, cell, True)
                if not quality: continue

                # strength
                strength = int(quality * self.strength / len(path))
                self.light_map[cell] = strength
                if not strength:
                    continue

                # new cells
                for direction in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                    # check that light can even travel in this direction
                    neighbor = self.level.ground.get_neighbor(cell_obj, direction)
                    if neighbor and neighbor.get('impassable'):
                        # we don't add the cell to the visited list just in case
                        # there's another path to it
                        continue

                    # add the new cell to the list to be processed
                    if neighbor.position not in visited:
                        visited[neighbor.position] = True
                        new.append(neighbor.position)

            cells = new


class CharacterLightSource(LightSource):
    '''Represent a single light source that lights the given level.
    '''
    def __init__(self, character, strength, level):
        self.character = character
        super(CharacterLightSource, self).__init__(character.position, strength, level)

class LightLayer(tiles.ScrollableLayer):
    def __init__(self):
        super(LightLayer, self).__init__()
        self.light_map = {}
        self.corners = {}

    def initialise(self):
        print 'INIT LIGHTMAP',
        t = time.time()
        level = self.parent.parent
        self.lights = []
        for c in level.party.characters:
            self.lights.append(CharacterLightSource(c, 128, level))
        print '...', str(time.time()-t), 'seconds'

        print 'CALC LIGHTMAP',
        t = time.time()
        for l in self.lights:
            for k, v in l.light_map.items():
                # don't truncate otherwise we can't subtract later
                self.light_map[k] = self.light_map.get(k, 0) + v
        print '...', str(time.time()-t), 'seconds'

        # force re-gen of corners
        self.corners = {}

    def set_view(self, x, y, w, h):
        super(LightLayer, self).set_view(x, y, w, h)

        # XXX actually we want to do this for light sources, not the
        # characters (unless they have a light source)
        level = self.parent.parent

        self.batch = pyglet.graphics.Batch()
        bv = []
        bc = []
        cells = level.ground.get_visible_cells()
        print 'LIGHTMAP QUADS',
        t = time.time()
        for tile in cells:
            # position of each corner
            x, y = tile.origin
            w, h = tile.width, tile.height
            x2, y2 = x+w, y+h
            bv.extend([x, y, x2, y, x2, y2, x, y2])

            # figure the lighting for each corner
            if tile.position not in self.corners:
                i, j = tile.position
                c = []
                for a, b in ((-1, -1), (0, -1), (0, 0), (-1, 0)):
                    ia = i+a; ia1 = i+a+1
                    jb = j+b; jb1 = j+b+1
                    o = []
                    for p in ((ia, jb), (ia1, jb), (ia1, jb1), (ia, jb1)):
                        o.append(min(255, self.light_map.get(p, 0)))
                    o = 255 - int(255 * (sum(o)/4.) / 256.)
                    if 0:
                        # for debugging - just set to the brightness of the cell
                        o = 255 - int(255 * self.light_map.get(tile.position, 0) / 256.)
                    c.extend([0, 0, 0, o])
                self.corners[tile.position] = c
            else:
                c = self.corners[tile.position]
            bc.extend(c)

        self.batch.add(4*len(cells), pyglet.gl.GL_QUADS, None,
            ('v2i', bv), ('c4B', bc),
        )
        print '...', str(time.time()-t), 'seconds'

    def on_character_move(self, character, old_position, position):
        # update light_map
        print 'UPDATE LIGHT MAP FOR', character,
        t = time.time()
        for l in self.lights:
            if l.character is character:
                self.remove_light(l)
                l.position = character.position
                l.light()
                self.add_light(l)
        print '...', str(time.time() - t), 'seconds'

    def remove_light(self, l):
        for k, v in l.light_map.items():
            # don't truncate otherwise we can't subtract later
            self.light_map[k] = self.light_map.get(k, 0) - v
            # clear out the cache of quad lighting values that this cell affects
            i, j = k
            for p in ((i+1, j), (i-1, j), (i, j+1), (i, j-1)):
                if p in self.corners:
                    del self.corners[p]

    def add_light(self, l):
        for k, v in l.light_map.items():
            # don't truncate otherwise we can't subtract later
            self.light_map[k] = self.light_map.get(k, 0) + v
            # clear out the cache of quad lighting values that this cell affects
            i, j = k
            for p in ((i+1, j), (i-1, j), (i, j+1), (i, j-1)):
                if p in self.corners:
                    del self.corners[p]

class FirstRunLayer(dialog.DialogLayer):
    def setup(self):
        image = pyglet.resource.image('dialog-tac-help.png')
        self.add(cocos.sprite.Sprite(image,
            position=(self.width//2, self.height//2),
            anchor=(image.width//2, image.height//2)))


class NPCTurnLayer(dialog.DialogLayer):
    def setup(self):
        self.label = cocos.text.Label('Processing NPC Turn: 0%',
            color=(0, 0, 0, 255), font_size=24, bold=True,
            position=(self.width//2, self.height),
            anchor_x='center', anchor_y='top')
        self.add(self.label)

    done = 0
    def update(self):
        self.done += 1
        n = self.done * 100. / self.total
        self.label.element.text = 'Processing NPC Turn: %d%%'%n

    def cleanup(self):
        self.label = None


class Level(cocos.scene.Scene):
    '''

    How the tactical game works
    ---------------------------

    The player and computer take turns.

    At the start of the level, right at the end of initialisation,
    each character (PC and NPC) has their start_new_level() method
    invoked.

    The player's turn consists of:

    1. invoke start_new_turn on all of the player's characters
    2. open up the tactical_ui Interface for the first character
    3. handle any actions invoked through the Interface
    4. when space is hit, load up that Interface for the next character
    5. once all characters have been handled, the game switches to NPC turn

    The NPC turn consists of:

    1. invoke start_new_turn on all of the player's characters
    2. invoke run for each NPC in turn*
    3. once all NPCs are handled switch back to PC turn

    * important to note here is that the NPCs aren't just handled in a loop
    as that might consume the CPU for some noticeable period of time. Instead
    after each NPC is handled we set up a callback to handled another one.
    This gives the rest of the program (rendering, event handling) a chance
    to do something in between handling NPCs.

    '''

    PLAYER_TURN = 'player'
    NPC_TURN = 'npc'

    def __init__(self, player_party, name, first_run=False):
        super(Level, self).__init__()

        self.party = player_party

        # Basic scrolling manager. All layers added to this layer will be scrolled
        # when we set_focus() on it.
        self.scroller = tiles.ScrollingManager()
        self.add(self.scroller)

        # add in the level's ground layer loaded by the tile code
        self.ground = tiles.load(name)['ground']
        self.scroller.add(self.ground)

        # add in the info layer for display over the top of everything else
        # Z sorted above characters
        self.ground_info = GroundInfoLayer()
        self.scroller.add(self.ground_info, z=self.ground.origin_z+4)

        # add in NPCs and figure the PC spawn points using the properties
        # from the XML map file
        self.npcs = []
        self.spawns = []

        # XXX the following is for creating the map when first encountered, we also
        # XXX need a load/save
        for column in self.ground.cells:
            for cell in column:
                if cell.get('npc-class'):
                    self.npcs.append(npc.create(cell))
                elif cell.get('player-spawn'):
                    self.spawns.append(cell)

        # start PCs in random spots
        random.shuffle(self.spawns)

        # a layer to display character stuff (their sprites, mostly)
        self.character_layer = CharacterLayer()
        self.scroller.add(self.character_layer, z=self.ground.origin_z+1)
        for n in self.npcs:
            self.character_layer.add_npc(n)
        self.active_character = None

        for character in self.party.characters:
            s = self.spawns.pop()
            character.position = (s.i, s.j)
            self.character_layer.add_character(character)
        self.player_ui = None

        for n in self.npcs:
            n.start_new_level(self)
        for character in self.party.characters:
            character.start_new_level(self)

        self.light_layer = LightLayer()
        self.scroller.add(self.light_layer, z=self.ground.origin_z+3)
        self.light_layer.initialise()

    def get_at_cell(self, i, j, ignore=None):
        c = self.character_layer.get_at_cell(i, j)
        return c

    def get_cell(self, i, j, ignore=None):
        '''Sometimes (ok, just in map solving) we need to ignore some contents of the
        level.
        '''
        ground_cell = self.ground.get_cell(i, j)

        # see whether a character affects that cell
        c = self.character_layer.get_at_cell(i, j)
        if ignore and c in ignore:
            cell = ground_cell
        elif c:
            # make this cell impassable
            d = ground_cell.properties.copy()
            d['impassable'] = True
            cell = RectCell(i, j, 64, 64, d, ground_cell.tile)
        else:
            cell = ground_cell
        return cell

    UP = (0, 1)
    DOWN = (0, -1)
    LEFT = (-1, 0)
    RIGHT = (1, 0)
    def get_neighbor(self, cell, direction, ignore=None):
        '''Sometimes (ok, just in map solving) we need to ignore some contents of the
        level.
        '''
        dx, dy = direction
        return self.get_cell(cell.i + dx, cell.j + dy, ignore=ignore)

    # XXX player will want to be abe to make this go away
    first_run = True

    def on_enter(self):
        super(Level, self).on_enter()

        # add the user interface for interacting with the character
        # not added to scrolling manager so it's not scrolled
        self.player_ui = TacticalInterface(None)
        self.add(self.player_ui, z=10)

        # make the things listen for player events (like actions
        # used, movement or DEATH :)
        for character in self.party.characters:
            character.push_handlers(self.player_ui)
            character.push_handlers(self.light_layer)
            character.push_handlers(self)

        self.check_npc_visibility()

        self.start_player_turn()

        self.music_player = music.play('music_dungeon_t.ogg')

        # add help layer now so it's on top of the event stack
        if self.first_run:
            self.add(FirstRunLayer(), z=100)
            self.first_run = False

    def on_character_death(self, character):
        # check failure
        if not [c for c in self.party.characters if not c.is_dead()]:
            director.replace(cocos.scene.Scene(Failure('You   Failed!')))
            return

    def on_exit(self):
        super(Level, self).on_exit()

        for character in self.party.characters:
            # pop me
            character.pop_handlers()
            # pop the player ui
            character.pop_handlers()

        self.remove(self.player_ui)
        self.player_ui = None

        # XXX fade volume down and fade menu out
        self.music_player.pause()
        self.music_player = None

    def start_player_turn(self):
        '''Called when the player takes control of the game.
        '''
        self.mode = self.PLAYER_TURN

        # initialise their per-turn state
        for c in self.party.characters:
            c.start_new_turn(self)
            self.player_ui.update_char_consumables(c)

        # queue up the first character for control
        self.active_char_index = -1

        # start the NPC turn
        self.start_npc_turn()

    def start_npc_turn(self):
        '''Analyse the PC disposition and figure how to organise
        NPC response.'''
        npc.start_turn(self)
        self.next_player_character()

    def next_player_character(self):
        '''Invoked to line up a new character for the player to control, or to pass
        control to NPC when the player is all done.
        '''

        # get the next non-dead character in sequence
        if not [c for c in self.party.characters if not c.is_dead()]:
            print 'ALL DEAD! GAME OVER!'
        while 1:
            self.active_char_index = (self.active_char_index + 1)%4
            c = self.party.characters[self.active_char_index]
            if not c.is_dead():
                return self.activate_character(c)

    def activate_character(self, c):
        '''Make the given character be the currently controlled one.
        '''
        self.ground_info.clear()
        self.active_char_index = self.party.characters.index(c)

        # activate the character
        self.active_character = c
        self.player_ui.activate_character(c)

        # and re-focus the display on the center of the character's cell
        cell = self.ground.get_cell(*c.position)
        self.scroller.set_focus(*cell.center)
        self.update_visible_npcs()

    def update_visible_npcs(self):
        for k, v in self.character_layer.npc_sprites.items():
            if not v.opacity: continue

    def end_turn(self):
        '''Invoked to end the player's turn and pass control over to NPC.
        '''
        self.ground_info.clear()
        self.player_ui.activate_character(None)

        # the player is all done, switch to NPC
        self.mode = self.NPC_TURN

        # figure NPCs to process
        npc_left = [c for c in self.npcs if c.is_active()]
        self.npc_to_process = npc_left
        for n in npc_left:
            n.start_new_turn(self)

        self.npc_l = NPCTurnLayer(persistent=True)
        self.npc_l.total = len(npc_left)
        self.add(self.npc_l, z=100)

        # Add a callback to process the next NPC so that the rest of
        # the game (user event handling, rendering, animations, etc)
        # can have a go - Cocos will automatically call this as part
        # of the next clock tick in its event loop.
        self.do(Delay(0.5) + CallFunc(self.next_non_player_character))

    def next_non_player_character(self):
        '''Invoked to process the turn for a non-player-character, or to pass control
        back to the player if the NPCs are all done.
        '''
        if self.npc_to_process:
            n = self.npc_to_process.pop()
            s = self.character_layer.get_npc_sprite(n)
            n.run(self)
            if self.npc_l.label:
                self.npc_l.update()
        else:
            self.remove(self.npc_l)
            self.start_player_turn()

    def check_npc_visibility(self):
        for npc in self.npcs:
            visible = []
            for c in self.party.characters:
                src  = c.position
                dest = npc.position
                visible.append(bool(los.LineOfSight(self, src, dest, True)))

            if any(visible):
                o = 255
            elif '--god' in sys.argv:
                o = 100
            else:
                o = 0

            s = self.character_layer.get_npc_sprite(npc)
            s.opacity = o
            for s in s.get_children():
                if isinstance(s, cocos.sprite.Sprite):
                    s.opacity = o


class Failure(menustyle.MyStyleMenu):
    def on_enter(self):
        super(Failure, self).on_enter()

        # ugh, menu should DO this
        for child in self.get_children():
            self.remove(child)

        options = [
            MenuItem('Press    enter', self.on_quit),
        ]

        self.create_menu(options)

    def on_quit(self):
        director.pop();director.pop()


class TacticalInterface(ui.Root):
    '''This interface is *always* a direct child of the Level Scene (as defined
    in the level module.

    This means the "parent" attribute is that Level, giving us access to the
    map information and all the other characters etc.
    '''
    is_event_handler = True

    def __init__(self, character):
        style = ui.default.copy()
        style.background.fill_color = (0, 0, 0, 0)
        super(TacticalInterface, self).__init__(style=style)
        self.character = character

    _desired_scale = 1
    def on_mouse_scroll(self, x, y, dx, dy):
        if dy < 0:
            if self._desired_scale < .5: return True
            self._desired_scale -= .05
        elif dy > 0:
            if self._desired_scale > 1: return True
            self._desired_scale += .05
        if dy:
            level = self.parent
            level.scroller.do(ScaleTo(self._desired_scale, .1))
            return True

    def on_key_press(self, key, modifiers):
        level = self.parent
        if modifiers & pyglet.window.key.MOD_SHIFT:
            fx, fy = level.scroller.fx, level.scroller.fy
            if key == pyglet.window.key.MOTION_LEFT:
                fx -= 64
                level.scroller.set_focus(fx, fy)
                level.update_visible_npcs()
                return True
            elif key == pyglet.window.key.MOTION_RIGHT:
                fx += 64
                level.scroller.set_focus(fx, fy)
                level.update_visible_npcs()
                return True
            elif key == pyglet.window.key.MOTION_DOWN:
                fy -= 64
                level.scroller.set_focus(fx, fy)
                level.update_visible_npcs()
                return True
            elif key == pyglet.window.key.MOTION_UP:
                fy += 64
                level.scroller.set_focus(fx, fy)
                level.update_visible_npcs()
                return True
            return False

        elif modifiers:
            return False

        if key == pyglet.window.key.ENTER:
            self.parent.end_turn()
            return True

        # not currently controlling a character
        if self.character is None:
            return False

        i, j = self.character.position
        cell = level.get_cell(i, j, ignore=[self.character])
        new_cell = None
        if key == pyglet.window.key.MOTION_LEFT:
            new_cell = level.get_neighbor(cell, level.LEFT)
            if new_cell and new_cell.get('impassable'):
                new_cell = None
        elif key == pyglet.window.key.MOTION_RIGHT:
            new_cell = level.get_neighbor(cell, level.RIGHT)
            if new_cell and new_cell.get('impassable'):
                new_cell = None
        elif key == pyglet.window.key.MOTION_DOWN:
            new_cell = level.get_neighbor(cell, level.DOWN)
            if new_cell and new_cell.get('impassable'):
                new_cell = None
        elif key == pyglet.window.key.MOTION_UP:
            new_cell = level.get_neighbor(cell, level.UP)
            if new_cell and new_cell.get('impassable'):
                new_cell = None
        else:
            return False

        if new_cell is not None:
            # XXX figure actual cost here when we have real costs
            self.character.move_to((new_cell.i, new_cell.j), 1)

        return True

    def on_enter(self):
        super(TacticalInterface, self).on_enter()

        w, h = director.get_window_size()

        layout = ui.Column(self, x=2, height=h, spacing=2)

        level = self.parent
        bg_image = pyglet.resource.image('button-char-bg.png')
        for n, character in enumerate(self.parent.party.characters):
            name = character.name

            bg = ui.Image(layout, bg_image, id='%s-bg'%name,
                on_click=lambda w,x,y,b,m, c=character: level.activate_character(c))
            if n:
                bg.image.opacity = 128
            else:
                bg.image.opacity = 255

            image = pyglet.resource.image(character.sprite_name)
            scale = 32. / image.height
            # character sprite is anchored middle
            y = bg.y + 32 + image.anchor_y
            ui.Image(bg, image=image, x=20, y=y, scale=scale)

            sub = ui.Column(bg, x=36, y=bg.y-2, spacing=2)

            # force DPI to overried text scaling
            ui.Text(sub, 'AP: %d/%d'%(character.actions_remaining,
                character.max_actions), id='%s-ap'%name, dpi=96, font_size=10)
            ui.Text(sub, 'HP: %d/%d'%(character.health, character.max_health),
                id='%s-hp'%name, dpi=96, font_size=10)
            ui.Text(sub, 'MP: %d/%d'%(character.mana, character.max_mana),
                id='%s-mp'%name, dpi=96, font_size=10)

            ui.Text(bg, text=name, x=bg.x+bg.width//2, y=bg.y+2, font_size=10,
                dpi=96, anchor_x='center')

        # various top-level action buttons now
        layout = ui.Row(self, valign=ui.BOTTOM, spacing=2)
        ui.Image(layout, 'button-manage-party.png', on_click=lambda *a:
            director.push(inventory.CharacterInventoryScreen(
                self.parent.party, self.character)))
        ui.Image(layout, 'button-next-char.png',
            on_click=lambda *a:self.parent.next_player_character())
        ui.Image(layout, 'button-end-turn.png',
            on_click=lambda *a:self.parent.end_turn())
        ui.Image(layout, 'button-retreat.png',
            on_click=lambda *a:director.pop())

    def on_exit(self):
        super(TacticalInterface, self).on_exit()
        if self._dragging:
            director.window.set_mouse_cursor()

    def activate_character(self, character):
        self._move_highlight_current_cell = None
        if self.character is not None:
            vl = self.by_id['%s-bg'%self.character.name]
            vl.image.opacity = 128

        self.character = character
        if character is not None:
            vl = self.by_id['%s-bg'%self.character.name]
            vl.image.opacity = 255
            # XXX remove me self.update_char_action_overlay()
            self.parent.ground_info.clear()

    def on_text(self, text):
        if text == 'i':
            director.push(inventory.CharacterInventoryScreen(self.parent.party,
                self.character))
            return True
        if text == ' ':
            self.parent.next_player_character()
            return True

    def on_mouse_press(self, x, y, button, modifiers):
        if super(TacticalInterface, self).on_mouse_press(x, y, button, modifiers):
            return True
        if button & pyglet.window.mouse.RIGHT:
            return self.generate_popup_menu(x, y)
        elif button & pyglet.window.mouse.LEFT:
            self._drag_start = (x, y)
        return True

    def generate_popup_menu(self, x, y):
        '''Attempt to generate an action menu for the current character and
        the cell/other positioned (as best fit) at x, y on screen.

        Return boolean whether we generated a menu or not (no actions).
        '''
        level = self.parent

        # translate screen pixel to map pixel
        mx, my = level.scroller.pixel_from_screen(x, y)

        # get the cell
        cell = level.ground.get_at_pixel(mx, my)

        # and any creature in that cell
        # XXX one day we might have actions to perform on corpses
        other = level.character_layer.get_at_pixel(mx, my, only_alive=True)

        # right, now get the action options for the character
        options = self.character.get_action_options()
        menu = []
        for o in options:
            # run the check first as it might update cost, label, who knows
            ok = o.check(level, self.character, cell, other)

            label = '%s (%d AP)'%(o.label, o.cost)
            if not ok or o.chance == 0:
                menu.append(((o.type, o.name), label, False))
            elif o.chance != 1:
                label = '%s %d%% (%d AP)'%(o.label, o.chance * 100, o.cost)
                menu.append(((o.type, o.name), label, True))
            else:
                menu.append(((o.type, o.name), label, True))

        if not menu:
            return

        def on_close(selected, options=options, self=self):
            print 'SELECTED ACTION', selected
            for o in options:
                if (o.type, o.name) == selected:
                    print (o, o.type, o.name)
                    self.action_in_progress()
                    o.do(self.action_completed)
                    return

        menu = popup_menu.PopupMenu(x, y, menu, on_close)
        self.add(menu, z=1)
        return True

    _dragging = False
    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        if not buttons & pyglet.window.mouse.LEFT:
            return False

        ox, oy = self._drag_start
        if abs(ox-x) + abs(oy-y) < 2: return False

        if not self._dragging:
            # set the mouse cursor to indicate dragging
            cursor = director.window.get_system_mouse_cursor(director.window.CURSOR_HAND)
            director.window.set_mouse_cursor(cursor)
            self._dragging = True

        level = self.parent
        fx, fy = level.scroller.fx, level.scroller.fy
        fx -= dx/self._desired_scale
        fy -= dy/self._desired_scale
        level.scroller.set_focus(fx, fy)
        level.update_visible_npcs()
        return True

    _mouse_current_cell = None
    def on_mouse_motion(self, x, y, dx, dy):
        self.update_char_action_overlay()

    def on_mouse_release(self, x, y, button, modifiers):
        if super(TacticalInterface, self).on_mouse_release(x, y, button, modifiers):
            return True

        if not button & pyglet.window.mouse.LEFT:
            # don't care about other buttons
            return False

        # LMB released
        if self._dragging:
            # reset mouse cursor
            director.window.set_mouse_cursor()
            self._dragging = False
        else:
            self._dragging = False
            level = self.parent

            if self._current_cell_action is not None:
                self.action_in_progress()
                self._current_cell_action.do(self.action_completed)
                return True
            elif self._move_highlight_current_cell:
                # current cell is a valid move
                cell = self._move_highlight_current_cell
                path = pathfinding.find_path(level, self.character.position,
                    cell.position)
                if not path:
                    return False
                # path totalCost includes current cell
                self.character.move_to(cell.position, path.totalCost-1)
                return True

            # might be a click to select another PC
            mx, my = level.scroller.pixel_from_screen(x, y)
            other = level.character_layer.get_at_pixel(mx, my, only_alive=True)
            if isinstance(other, party.PC):
                level.activate_character(other)
                return True

    def update_char_consumables(self, character):
        self.by_id['%s-ap'%character.name].text = 'AP: %d/%d'%(
            character.actions_remaining, character.max_actions)
        self.by_id['%s-mp'%character.name].text = 'MP: %d/%d'%(
            character.mana, character.max_mana)

    def on_character_action_used(self, character, points_left, points_used):
        self.by_id['%s-ap'%character.name].text = 'AP: %d/%d'%(
            character.actions_remaining, character.max_actions)
        # XXX remove me self.update_char_action_overlay()
        self.parent.ground_info.clear()
        # no return in case someone else wants the event

    def on_character_mana_used(self, character, points_left, points_used):
        self.by_id['%s-mp'%character.name].text = 'MP: %d/%d'%(
            character.mana, character.max_mana)
        # no return in case someone else wants the event

    def on_character_damage(self, character, old_health, damage):
        self.by_id['%s-hp'%character.name].text = 'HP: %d/%d'%(
            character.health, character.max_health)
        # no return in case someone else wants the event

    def on_character_move(self, character, old_pos, new_pos):
        # highlight the cells the player may move to
        print 'CHAR MOVED!'
        # XXX remove me self.update_char_action_overlay()
        self.parent.ground_info.clear()

    _block_player_action_highlight = False
    def action_in_progress(self):
        self._block_player_action_highlight = True

    def action_completed(self):
        self._block_player_action_highlight = False

    _move_highlight_current_cell = None
    _current_action_cell = None
    _current_cell_action = None
    def update_char_action_overlay(self):
        # XXX move this into the ground info layer

        # no character to do move for
        if not self.character:
            return

        w = director.window
        # ugh, pyglet, why do you ask this of me?
        x, y = w._mouse_x, w._mouse_y

        level = self.parent

        # translate screen pixel to map pixel
        mx, my = level.scroller.pixel_from_screen(x, y)
        cell = level.ground.get_at_pixel(mx, my)
        other = level.character_layer.get_at_pixel(mx, my, only_alive=True)

        path = []
        action = None

        # maybe over an NPC?
        if isinstance(other, npc.NPC) and not self._block_player_action_highlight:
            if self._current_action_cell is cell:
                return
            self._current_action_cell = cell

            cx, cy = self.character.position
            nx, ny = other.position
            # attack!
            if abs(cx - nx) > 1 or abs(cy - ny) > 1:
                action = self.character.get_ranged_attack(level, cell, other)
                print 'RANGED ATTACK', action
            else:
                action = self.character.get_melee_attack(level, cell, other)
                print 'MELEE ATTACK', action
            self._current_cell_action = action
            self._move_highlight_current_cell = None
            if action is None:
                class NoActionPossible:
                    icon = 'icon-miss.png'
                action = NoActionPossible
        elif isinstance(other, party.PC):
            # XXX do PC highlight here
            self._current_cell_action = None
            self._current_action_cell = None
            self._move_highlight_current_cell = None

        else:
            # get the cell
            if cell is self._move_highlight_current_cell:
                return
            self._move_highlight_current_cell = cell

            self._current_cell_action = None
            self._current_action_cell = None

            if cell.position == self.character.position:
                level.ground_info.clear()

            path = pathfinding.find_path(level, self.character.position,
                cell.position, close_enough=True)
            if path:
                path = [node.location for node in path.nodes[:self.character.actions_remaining]]
            else:
                path = []

        # update the info layer
        level.ground_info.update_for_character(self.character, path, cell, action)

    def draw(self):
        self.transform()
        self.batch.draw()

